import math

from basis import *
from line import Line


class Subpath:
    id_num = 0
    offset_amount = 0.

    def __init__(self, x: float, y: float):
        self.sx = x
        self.sy = y
        self.cx = x
        self.cy = y
        self.ix = x
        self.iy = y
        self.last_cubic_cp_x = None
        self.last_cubic_cp_y = None
        self.last_quad_cp_x = None
        self.last_quad_cp_y = None
        self.elements = []  # type: list[Line]
        self.is_inner = False
        self.is_outer = False
        self.is_ignored = False
        self.is_analyzed = False
        self.is_closed = False
        self.start_offset_at_idx = None  # type: Optional[int]
        self.start_offset_right = None  # type: Optional[bool]
        Subpath.id_num += 1
        self.id = Subpath.id_num

    def add_element(self, e: Line) -> None:
        assert e.y1 >= 0 and e.y2 >= 0, e
        if len(self.elements) > 0:
            assert e.x1 == self.cx and e.y1 == self.cy, (self.cx, self.cy, e, self.elements[-1])
            # numbers from svg file can be non-representable exactly in IEEE754, hence there can be slight error which,
            #  if not fixed, can lead to strange extremely short lines which cause troubles offsetting them
            if abs(e.x2 - e.x1) < Subpath.offset_amount/4 and abs(e.y2 - e.y1) < Subpath.offset_amount/4:
                e = Line(False, self.elements[-1].x1, self.elements[-1].y1, e.x2, e.y2)
                del self.elements[-1]
        self.elements.append(e)
        self.cx, self.cy = e.last_point()

    def add_elements(self, elements: list[Line]) -> None:
        self.elements.extend(elements)
        # this function is used when adding lines from interpolation, no need to fix rounding errors
        self.cx, self.cy = elements[-1].last_point()

    @staticmethod
    def from_line(line: Line) -> 'Subpath':
        subpath = Subpath(line.x1, line.y1)
        subpath.add_element(line)
        return subpath

    @staticmethod
    def from_lines(lines: list[Line]):
        for i in range(len(lines)-1):
            assert lines[i].x2 == lines[i+1].x1 and lines[i].y2 == lines[i+1].y1, (lines[i], lines[i+1], i)
        subpath = Subpath(lines[0].x1, lines[0].y1)
        subpath.add_elements(lines)
        return subpath

    def lineto(self, is_relative: bool, x: float, y: float):
        e = Line(is_relative, self.cx, self.cy, x, y)
        self.add_element(e)
        self.last_cubic_cp_x = None
        self.last_cubic_cp_y = None
        self.last_quad_cp_x = None
        self.last_quad_cp_y = None

    def h_line(self, is_relative: bool, x: float):
        self.lineto(is_relative, x, 0 if is_relative else self.cy)

    def v_line(self, is_relative: bool, y: float):
        self.lineto(is_relative, 0 if is_relative else self.cx, y)

    def cubic(self, is_relative: bool, x1: float, y1: float, x2: float, y2: float, x: float, y: float):
        e = Line.interpolate_cubic(is_relative, self.cx, self.cy, x1, y1, x2, y2, x, y)
        self.add_elements(e)
        self.last_cubic_cp_x = x2
        self.last_cubic_cp_y = y2
        self.last_quad_cp_x = None
        self.last_quad_cp_y = None

    def cubic_smooth(self, is_relative: bool, x2: float, y2: float, x: float, y: float):
        self.cubic(is_relative, Subpath.cp_reflect(self.cx, self.last_cubic_cp_x, is_relative),
                   Subpath.cp_reflect(self.cy, self.last_cubic_cp_y, is_relative), x2, y2, x, y)

    def quad(self, is_relative: bool, x1: float, y1: float, x: float, y: float):
        e = Line.interpolate_quad(is_relative, self.cx, self.cy, x1, y1, x, y)
        self.add_elements(e)
        self.last_cubic_cp_x = None
        self.last_cubic_cp_y = None
        self.last_quad_cp_x = x1
        self.last_quad_cp_y = y1

    def quad_smooth(self, is_relative: bool, x: float, y: float):
        self.quad(is_relative, Subpath.cp_reflect(self.cx, self.last_cubic_cp_x, is_relative),
                  Subpath.cp_reflect(self.cy, self.last_cubic_cp_y, is_relative), x, y)

    def arc(self, is_relative: bool, rx: float, ry: float, x_axis_rotation: float, large_arc_flag: bool,
            sweep_flag: bool, x: float, y: float):
        e = Line.interpolate_arc(is_relative, self.cx, self.cy, rx, ry, x_axis_rotation, large_arc_flag,
                                 sweep_flag, x, y)
        self.add_elements(e)
        self.last_cubic_cp_x = None
        self.last_cubic_cp_y = None
        self.last_quad_cp_x = None
        self.last_quad_cp_y = None

    def close(self):
        if self.cx != self.sx or self.cy != self.sy:
            self.lineto(False, self.sx, self.sy)
        self.is_closed = True

    def intersections(self, y: float, debug: bool = False) -> list[tuple[float, int, int]]:
        res = []
        seen_less = False
        seen_more = False
        for e_idx in range(len(self.elements)*2):
            e = self.elements[e_idx % len(self.elements)]
            if e.min_y <= y <= e.max_y:
                if e.min_y == e.max_y:  # ignore horizontal line, will be accounted by either previous or next line
                    if debug:
                        print(f' ignore horizontal line #{self.id} {y:0.2f} {e}')
                    continue
                if e.min_y < y < e.max_y or seen_more and e.min_y < y or seen_less and e.max_y > y:
                    e_x = e.leftmost_x(y)
                    direction = 1 if e.y2 > e.y1 else -1
                    if debug:
                        print(f' found non-vertex intersection #{self.id} {y:0.2f} e{e_idx} {e} {e_x:0.2f} {direction} (done {e_idx > len(self.elements) - 1})')
                    if e_idx >= len(self.elements) and not (seen_more or seen_less):
                        # only run second loop through elements if last element was vertex intersection
                        break
                    res.append((e_x, direction, e_idx))
                    seen_more = seen_less = False
                    # if self.id in (192, 193):
                    #     print(f' leftmost #{self.id} {y:0.2f} {e}: {e_x:0.2f} {direction} ({seen_more} {seen_less})')
                elif y < e.max_y:  # and y == e.min_y
                    seen_more = not seen_more
                    if debug:
                        print(f' found vertex intersection #{self.id} {y:0.2f} e{e_idx} {e} seen more {seen_more}')
                    # if self.id in (192, 193):
                    #     print(f' leftmost #{self.id} seen more {seen_more} {y:0.2f} {e}')
                elif y > e.min_y:  # and y == e.max_y
                    seen_less = not seen_less
                    if debug:
                        print(f' found vertex intersection #{self.id} {y:0.2f} e{e_idx} {e} seen less {seen_less}')
                    # if self.id in (192, 193):
                    #     print(f' leftmost #{self.id} seen less {seen_less} {y:0.2f} {e}')
            elif e_idx >= len(self.elements):
                if debug:
                    print(f' done #{self.id} {y:0.2f} e{e_idx} {e}')
                break
            else:
                if debug:
                    print(f' out of range #{self.id} {y:0.2f} e{e_idx} {e} {e.min_y} {e.max_y} {y >= e.min_y} {y <= e.max_y}')
                seen_more = seen_less = False
                # if self.id in (192, 193):
                #     print(f' leftmost #{self.id} out {seen_more} {seen_less} {y:0.2f} {e}')
        # if self.id in (192, 193):
        #     print(f' result #{self.id} {res}')
        return res

    def min(self) -> tuple[float, float]:
        x = math.inf
        y = math.inf
        for e in self.elements:
            if x > e.min_x:
                x = e.min_x
            if y > e.min_y:
                y = e.min_y
        return x, y

    def offset(self, amount: float) -> 'Subpath':
        # e.x2,e.y2 += amount*(-e*abs(e.next)+e.next*abs(e))/cross(-e,e.next)
        points = list()  # type: list[Vector]
        for i in range(len(self.elements)):
            e = self.elements[(i + self.start_offset_at_idx) % len(self.elements)]
            next_e = self.elements[(i + self.start_offset_at_idx + 1) % len(self.elements)]
            u = -Vector(line=e)
            v = Vector(line=next_e)
            udotv = u.dot_product(v)
            a = Vector(e.x2, e.y2)
            u1 = u / abs(u) * amount
            v1 = v / abs(v) * amount
            uxv = u.cross_product(v)
            if udotv >= 0:  # cos(alpha) > 0, alpha > -90 < 90
                if uxv > 0:
                    # outer offset need to draw linecap
                    points.append(a + u1.rotate_left_90())
                    points.append(a + - (u1 + v1) / 2)
                    points.append(a + v1.rotate_right_90())
                else:
                    # inner offset
                    points.append(a + - (u * abs(v) + v * abs(u)) / uxv * amount)
            else:
                if abs(uxv)/(abs(u)*abs(v)) < 0.0001:
                    # almost parallel same direction because u*v < 0
                    points.append(a + v1.rotate_right_90())
                else:
                    # non-parallel
                    points.append(a + - (u * abs(v) + v * abs(u)) / uxv * amount)  # problem is here
                    # if points[-1].y < 0:
                    #     print(f'{self.id} {i} {e} {next_e} {a} {u} {v} {u1} {v1} uxv {uxv} udotv {udotv} abs(v) {abs(v)} abs(u) {abs(u)} amount {amount}')
        res = Subpath(points[0].x, points[0].y)
        for p in points[1:]:
            res.lineto(False, p.x, p.y)
        res.close()
        # print(self.elements[0])
        # print(res.elements[0])
        res.start_offset_at_idx = len(res.elements) - 1  # self.start_offset_at_idx
        res.start_offset_right = self.start_offset_right
        return res

    def to_d(self) -> str:
        res = f'M {self.elements[0].x1:0.3f} {self.elements[0].y1:0.3f} '
        for e in self.elements:
            res += e.to_d()
            res += ' '
        if self.is_closed:
            res += 'Z'
        return res

    @staticmethod
    def cp_reflect(x: float, y: float, is_relative: bool) -> float:
        if y is None:
            y = x
        if is_relative:
            return x - y
        else:
            return x - (y - x)
